using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.Linq;

namespace Kotonoha.Objects.SuperMemo {

    public class OFMatrixHolder {
        public static OFMatrix Matrix;
    }

    public class OFMatrix {
        private Dictionary<int, Dictionary<double, OFMatrixItem>> dict = new Dictionary<int, Dictionary<double, OFMatrixItem>>();
        private Random random = new Random(Environment.TickCount % 32545123);

        public OFMatrix(IList<OFMatrixItem> items) {
            LoadMatrixItems(items);
        }

        private void LoadMatrixItems(IList<OFMatrixItem> items) {
            foreach (var item in items) {
                Process(item);
            }
        }

        private void Process(OFMatrixItem data) {
            Dictionary<double, OFMatrixItem> d;
            if (!dict.TryGetValue(data.N, out d)) {
                d = new Dictionary<double, OFMatrixItem>();
                dict.Add(data.N, d);
            }

            d[data.EF] = data;
        }

        private OFMatrixItem Get(int n, double ef) {
            ef = Math.Round(ef, 2);
            Dictionary<double, OFMatrixItem> d;
            if (!dict.TryGetValue(n, out d)) {
                d = new Dictionary<double, OFMatrixItem>();
                dict.Add(n, d);
            }

            OFMatrixItem it;
            if (!d.TryGetValue(ef, out it)) {
                it = new OFMatrixItem { Value = n == 1 ? 4 : ef, N = n, EF = ef };
                d.Add(ef, it);
            }
            return it;
        }

        public void Update(ItemLearningData data, double q, DateTime time) {
            double oldEf = data.Difficulty;
            double ef = Math.Max(1.3, oldEf + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)));
            data.Difficulty = ef;

            int n = data.Repetition;
            if (n == 0) {
                data.Repetition = 1;
                data.IntervalLength = Get(1, ef).Value;
                FinishData(data, time);
                return;
            }

            double il = data.IntervalLength;
            var OF = Get(n, ef);
            var oldOf = Get(n - 1, oldEf);
            double mod5 = Math.Max(1.05, (il + 1) / il);
            double mod2 = Math.Min(0.75, (il - 1) / il);
            double mod;
            if (q > 4) {
                mod = 1 + (mod5 - 1) * (q - 4);
            } else {
                mod = 1 - (mod2 - 1) / 2 * (4 - q);
            }

            double oldOfVal = OF.Value;
            double newOf = oldOf.Value * mod;
            bool change = (q > 4 && newOf > oldOfVal) || (q < 4 && newOf < oldOfVal);
            //We change values if answer was bad and OF dropped or if answer was good and OF grew.
            if (change) {
                OF.Value = Math.Max(1.2, oldOfVal * 0.5 + newOf * 0.5);
            }

            if (q < 3.5) {
                ++data.Lapse;
                data.Repetition = 1;
                data.IntervalLength = Get(1, ef).Value;
            } else {
                ++data.Repetition;
                const double a = 0.047;
                const double b = 0.092;
                double p = random.NextDouble() - 0.5;
                double m = -1 / b * Math.Log(1 - b / a * Math.Abs(p));
                m = m * Math.Sign(p);
                //NOI:=PI*(1+(OF-1)*(100+m)/100);
                data.IntervalLength = data.IntervalLength * (1 + (Get(n + 1, ef).Value - 1) * (100 + m) / 100);
            }

            FinishData(data, time);
            return;
        }

        private void FinishData(ItemLearningData data, DateTime time) {
            data.IntervalStart = time;
            data.IntervalEnd = time.AddDays(data.IntervalLength);
        }

        public List<OFMatrixItem> Dump() {
            return dict.SelectMany(p => p.Value).Select(p => p.Value).ToList();
        }
    }

    public class OFMatrixItem : IEquatable<OFMatrixItem> {
        [XmlAttribute]
        public int N { get; set; }
        [XmlAttribute]
        public double EF { get; set; }
        [XmlAttribute]
        public double Value { get; set; }

        public bool Equals(OFMatrixItem other) {
            if (ReferenceEquals(null, other)) {
                return false;
            }
            if (ReferenceEquals(this, other)) {
                return true;
            }
            return other.N == N && other.EF == EF;
        }

        public override bool Equals(object obj) {
            if (ReferenceEquals(null, obj)) {
                return false;
            }
            if (ReferenceEquals(this, obj)) {
                return true;
            }
            if (obj.GetType() != typeof(OFMatrixItem)) {
                return false;
            }
            return Equals((OFMatrixItem)obj);
        }

        public override int GetHashCode() {
            unchecked {
                return (N * 397) ^ EF.GetHashCode();
            }
        }
    }
}